// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.

namespace WixToolset.Core.WindowsInstaller.Bind
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using WixToolset.Data;
    using WixToolset.Data.Symbols;
    using WixToolset.Data.WindowsInstaller;

    internal class ReduceTransformCommand
    {
        public ReduceTransformCommand(Intermediate intermediate, IEnumerable<PatchTransform> patchTransforms, PatchFilterMap patchFilterMap)
        {
            this.Intermediate = intermediate;
            this.PatchTransforms = patchTransforms;
            this.PatchFilterMap = patchFilterMap;
        }

        private Intermediate Intermediate { get; }

        private IEnumerable<PatchTransform> PatchTransforms { get; }

        private PatchFilterMap PatchFilterMap { get; }

        public void Execute()
        {
            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList();

            var patchRefSymbols = symbols.OfType<WixPatchRefSymbol>().ToList();

            if (patchRefSymbols.Count > 0)
            {
                foreach (var patchTransform in this.PatchTransforms)
                {
                    if (!this.ReduceTransform(patchTransform.Transform, patchRefSymbols))
                    {
                        // transform has none of the content authored into this patch
                        continue;
                    }
                }
            }
        }

        /// <summary>
        /// Reduce the transform according to the patch references.
        /// </summary>
        /// <param name="transform">transform generated by torch.</param>
        /// <param name="patchRefSymbols">Table contains patch family filter.</param>
        /// <returns>true if the transform is not empty</returns>
        private bool ReduceTransform(WindowsInstallerData transform, IEnumerable<WixPatchRefSymbol> patchRefSymbols)
        {
            // identify sections to keep
            var targetFilterIdsToKeep = new Dictionary<string, Row>();
            var updatedFilterIdsToKeep = new Dictionary<string, Row>();
            var tableKeyRows = new Dictionary<string, Dictionary<string, Row>>();
            var sequenceList = new List<Table>();
            var componentFeatureAddsIndex = new Dictionary<string, List<string>>();
            var customActionTable = new Dictionary<string, Row>();
            var directoryTableAdds = new Dictionary<string, Row>();
            var featureTableAdds = new Dictionary<string, Row>();
            var keptComponents = new Dictionary<string, Row>();
            var keptDirectories = new Dictionary<string, Row>();
            var keptFeatures = new Dictionary<string, Row>();
            var keptLockPermissions = new HashSet<string>();
            var keptMsiLockPermissionExs = new HashSet<string>();

            var componentCreateFolderIndex = new Dictionary<string, List<string>>();
            var directoryLockPermissionsIndex = new Dictionary<string, List<Row>>();
            var directoryMsiLockPermissionsExIndex = new Dictionary<string, List<Row>>();

            foreach (var patchRefSymbol in patchRefSymbols)
            {
                var tableName = patchRefSymbol.Table;
                var primaryKey = patchRefSymbol.PrimaryKeys;

                // Short circuit filtering if all changes should be included.
                if ("*" == tableName && "*" == primaryKey)
                {
                    RemoveProductCodeFromTransform(transform);
                    return true;
                }

                if (!transform.Tables.TryGetTable(tableName, out var table))
                {
                    // Table not found.
                    continue;
                }

                // Index the table.
                if (!tableKeyRows.TryGetValue(tableName, out var rowsByPrimaryKey))
                {
                    rowsByPrimaryKey = table.Rows.ToDictionary(r => r.GetPrimaryKey());
                    tableKeyRows.Add(tableName, rowsByPrimaryKey);
                }

                if (!rowsByPrimaryKey.TryGetValue(primaryKey, out var row))
                {
                    // Row not found.
                    continue;
                }

                if (this.PatchFilterMap.TryGetPatchFiltersForRow(row, out var targetFilterId, out var updatedFilterId))
                {
                    targetFilterIdsToKeep[targetFilterId ?? String.Empty] = row;
                    updatedFilterIdsToKeep[updatedFilterId ?? String.Empty] = row;
                }
            }

            // throw away sections not referenced
            var keptRows = 0;
            Table directoryTable = null;
            Table featureTable = null;
            Table lockPermissionsTable = null;
            Table msiLockPermissionsTable = null;

            foreach (var table in transform.Tables)
            {
                if ("_SummaryInformation" == table.Name)
                {
                    continue;
                }

                if (table.Name == "AdminExecuteSequence"
                    || table.Name == "AdminUISequence"
                    || table.Name == "AdvtExecuteSequence"
                    || table.Name == "InstallUISequence"
                    || table.Name == "InstallExecuteSequence")
                {
                    sequenceList.Add(table);
                    continue;
                }

                for (var i = 0; i < table.Rows.Count; i++)
                {
                    var row = table.Rows[i];

                    if (table.Name == "CreateFolder")
                    {
                        var createFolderComponentId = row.FieldAsString(1);

                        if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList))
                        {
                            directoryList = new List<string>();
                            componentCreateFolderIndex.Add(createFolderComponentId, directoryList);
                        }

                        directoryList.Add(row.FieldAsString(0));
                    }

                    if (table.Name == "CustomAction")
                    {
                        customActionTable.Add(row.FieldAsString(0), row);
                    }

                    if (table.Name == "Directory")
                    {
                        directoryTable = table;
                        if (RowOperation.Add == row.Operation)
                        {
                            directoryTableAdds.Add(row.FieldAsString(0), row);
                        }
                    }

                    if (table.Name == "Feature")
                    {
                        featureTable = table;
                        if (RowOperation.Add == row.Operation)
                        {
                            featureTableAdds.Add(row.FieldAsString(0), row);
                        }
                    }

                    if (table.Name == "FeatureComponents")
                    {
                        if (RowOperation.Add == row.Operation)
                        {
                            var featureId = row.FieldAsString(0);
                            var componentId = row.FieldAsString(1);

                            if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList))
                            {
                                featureList = new List<string>();
                                componentFeatureAddsIndex.Add(componentId, featureList);
                            }

                            featureList.Add(featureId);
                        }
                    }

                    if (table.Name == "LockPermissions")
                    {
                        lockPermissionsTable = table;
                        if ("CreateFolder" == row.FieldAsString(1))
                        {
                            var directoryId = row.FieldAsString(0);

                            if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList))
                            {
                                rowList = new List<Row>();
                                directoryLockPermissionsIndex.Add(directoryId, rowList);
                            }

                            rowList.Add(row);
                        }
                    }

                    if (table.Name == "MsiLockPermissionsEx")
                    {
                        msiLockPermissionsTable = table;
                        if ("CreateFolder" == row.FieldAsString(1))
                        {
                            var directoryId = row.FieldAsString(0);

                            if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList))
                            {
                                rowList = new List<Row>();
                                directoryMsiLockPermissionsExIndex.Add(directoryId, rowList);
                            }

                            rowList.Add(row);
                        }
                    }

                    if (this.IsInPatchFamily(row, targetFilterIdsToKeep, updatedFilterIdsToKeep))
                    {
                        if ("Component" == table.Name)
                        {
                            keptComponents.Add(row.FieldAsString(0), row);
                        }

                        if ("Directory" == table.Name)
                        {
                            keptDirectories.Add(row.FieldAsString(0), row);
                        }

                        if ("Feature" == table.Name)
                        {
                            keptFeatures.Add(row.FieldAsString(0), row);
                        }

                        keptRows++;
                    }
                    else
                    {
                        table.Rows.RemoveAt(i);
                        i--;
                    }
                }
            }

            keptRows += this.ReduceTransformSequenceTable(sequenceList, targetFilterIdsToKeep, updatedFilterIdsToKeep, customActionTable);

            if (null != directoryTable)
            {
                foreach (var componentRow in keptComponents.Values)
                {
                    var componentId = componentRow.FieldAsString(0);

                    if (RowOperation.Add == componentRow.Operation)
                    {
                        // Make sure each added component has its required directory and feature heirarchy.
                        var directoryId = componentRow.FieldAsString(2);
                        while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow))
                        {
                            if (!keptDirectories.ContainsKey(directoryId))
                            {
                                directoryTable.Rows.Add(directoryRow);
                                keptDirectories.Add(directoryId, directoryRow);
                                keptRows++;
                            }

                            directoryId = directoryRow.FieldAsString(1);
                        }

                        if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds))
                        {
                            foreach (var featureId in componentFeatureIds)
                            {
                                var currentFeatureId = featureId;
                                while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow))
                                {
                                    if (!keptFeatures.ContainsKey(currentFeatureId))
                                    {
                                        featureTable.Rows.Add(featureRow);
                                        keptFeatures.Add(currentFeatureId, featureRow);
                                        keptRows++;
                                    }

                                    currentFeatureId = featureRow.FieldAsString(1);
                                }
                            }
                        }
                    }

                    // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept.
                    foreach (var keptComponentId in keptComponents.Keys)
                    {
                        if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList))
                        {
                            foreach (var directoryId in directoryList)
                            {
                                if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList))
                                {
                                    foreach (var lockPermissionsRow in lockPermissionsRowList)
                                    {
                                        var key = lockPermissionsRow.GetPrimaryKey('/');
                                        if (keptLockPermissions.Add(key))
                                        {
                                            lockPermissionsTable.Rows.Add(lockPermissionsRow);
                                            keptRows++;
                                        }
                                    }
                                }

                                if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList))
                                {
                                    foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList)
                                    {
                                        var key = msiLockPermissionsExRow.GetPrimaryKey('/');
                                        if (keptMsiLockPermissionExs.Add(key))
                                        {
                                            msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow);
                                            keptRows++;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            keptRows += this.ReduceTransformSequenceTable(sequenceList, targetFilterIdsToKeep, updatedFilterIdsToKeep, customActionTable);

            // Delete tables that are empty.
            var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name).ToList();

            foreach (var tableName in tablesToDelete)
            {
                transform.Tables.Remove(tableName);
            }

            return keptRows > 0;
        }

        private bool IsInPatchFamily(Row row, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections)
        {
            var result = false;

            if (this.PatchFilterMap.TryGetPatchFiltersForRow(row, out var targetFilterId, out var updatedFilterId))
            {
                if ((String.IsNullOrEmpty(targetFilterId) && newSections.ContainsKey(updatedFilterId)) || (String.IsNullOrEmpty(updatedFilterId) && oldSections.ContainsKey(targetFilterId)))
                {
                    result = true;
                }
                else if (!String.IsNullOrEmpty(targetFilterId) && !String.IsNullOrEmpty(updatedFilterId) && (oldSections.ContainsKey(targetFilterId) || newSections.ContainsKey(updatedFilterId)))
                {
                    result = true;
                }
            }

            return result;
        }

        /// <summary>
        /// Check if the section is in a PatchFamily.
        /// </summary>
        /// <param name="oldSection">Section id in target wixout</param>
        /// <param name="newSection">Section id in upgrade wixout</param>
        /// <param name="oldSections">Dictionary contains section id should be kept in the baseline wixout.</param>
        /// <param name="newSections">Dictionary contains section id should be kept in the upgrade wixout.</param>
        /// <returns>true if section in patch family</returns>
        private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections)
        {
            var result = false;

            if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection)))
            {
                result = true;
            }
            else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection)))
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Remove the ProductCode property from the transform.
        /// </summary>
        /// <param name="transform">The transform.</param>
        /// <remarks>
        /// Changing the ProductCode is not supported in a patch.
        /// </remarks>
        private static void RemoveProductCodeFromTransform(WindowsInstallerData transform)
        {
            if (transform.Tables.TryGetTable("Property", out var propertyTable))
            {
                for (var i = 0; i < propertyTable.Rows.Count; ++i)
                {
                    var propertyRow = propertyTable.Rows[i];
                    var property = propertyRow.FieldAsString(0);

                    if ("ProductCode" == property)
                    {
                        propertyTable.Rows.RemoveAt(i);
                        break;
                    }
                }
            }
        }

        /// <summary>
        /// Reduce the transform sequence tables.
        /// </summary>
        /// <param name="sequenceList">ArrayList of tables to be reduced</param>
        /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
        /// <param name="newSections">Hashtable contains section id should be kept in the target wixout.</param>
        /// <param name="customAction">Hashtable contains all the rows in the CustomAction table.</param>
        /// <returns>Number of rows left</returns>
        private int ReduceTransformSequenceTable(List<Table> sequenceList, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections, Dictionary<string, Row> customAction)
        {
            var keptRows = 0;

            foreach (var currentTable in sequenceList)
            {
                for (var i = 0; i < currentTable.Rows.Count; i++)
                {
                    var row = currentTable.Rows[i];
                    var actionName = row.FieldAsString(0);

                    if (row.Operation == RowOperation.None)
                    {
                        if (this.IsInPatchFamily(row, oldSections, newSections))
                        {
                            keptRows++;
                        }
                        else
                        {
                            currentTable.Rows.RemoveAt(i);
                            i--;
                        }
                    }
                    else if (row.Operation == RowOperation.Modify)
                    {
                        var sequenceChanged = row.Fields[2].Modified;
                        var conditionChanged = row.Fields[1].Modified;

                        if (sequenceChanged && !conditionChanged)
                        {
                            keptRows++;
                        }
                        else if (!sequenceChanged && conditionChanged)
                        {
                            if (this.IsInPatchFamily(row, oldSections, newSections))
                            {
                                keptRows++;
                            }
                            else
                            {
                                currentTable.Rows.RemoveAt(i);
                                i--;
                            }
                        }
                        else if (sequenceChanged && conditionChanged)
                        {
                            if (this.IsInPatchFamily(row, oldSections, newSections))
                            {
                                keptRows++;
                            }
                            else
                            {
                                row.Fields[1].Modified = false;
                                keptRows++;
                            }
                        }
                    }
                    else if (row.Operation == RowOperation.Delete)
                    {
                        if (this.IsInPatchFamily(row, oldSections, newSections))
                        {
                            keptRows++;
                        }
                        else
                        {
                            if (customAction.ContainsKey(actionName))
                            {
                                currentTable.Rows.RemoveAt(i);
                                i--;
                            }
                            else
                            {
                                // it is a stardard action, we should keep this action.
                                row.Operation = RowOperation.None;
                                keptRows++;
                            }
                        }
                    }
                    else if (row.Operation == RowOperation.Add)
                    {
                        // Keep unfiltered added rows.
                        if (!this.PatchFilterMap.ContainsPatchFilterForRow(row))
                        {
                            keptRows++;
                        }
                        else if (this.IsInPatchFamily(row, oldSections, newSections))
                        {
                            keptRows++;
                        }
                        else
                        {
                            if (customAction.ContainsKey(actionName))
                            {
                                currentTable.Rows.RemoveAt(i);
                                i--;
                            }
                            else
                            {
                                keptRows++;
                            }
                        }
                    }
                }
            }

            return keptRows;
        }
    }
}
